﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;

using SharedGenomics.Core;
using SharedGenomics.Annotations;

namespace SharedGenomics.Workbench.Annotations
{
    /// <summary>
    /// Repository for all annotations fetched by the workbench. It contains an <see cref="AnnotationPool"/> for each annotation type. AnnotationPools are 
    /// organised in a hierarchy, with the pool for the annotation target at the root (currently this pool is for SNP names). The pool can be queried to find 
    /// values in both directions i.e. to find an annotation of a particular type associated with the root or to find the root value for a particular annotation.
    /// </summary>
    public class AnnotationRepository
    {
        private readonly IDictionary<AnnotationKey, AnnotationPool> _keyPoolMap;
        private readonly AnnotationKeyDependencyGraph _dependencyGraph;

        public AnnotationRepository(AnnotationKeyDependencyGraph dependencyGraph)
        {
            dependencyGraph.ThrowIfNull("dependencyGraph");

            this._dependencyGraph = dependencyGraph;

            //create an annotation pool for each key
            //NOTE: the dependency graph should be immutable and therefore the collection of annotation pools should not change either
            this._keyPoolMap = dependencyGraph.ToDictionary(node => node.Value, node => new AnnotationPool());
        }

        public void Add(AnnotationKey key, object value, AnnotationValue parentValue)
        {
            if (key == null || value == null) return;

            //try to get the annotation pool for the given key and add the associated value(s)
            AnnotationPool pool;
            if (this._keyPoolMap.TryGetValue(key, out pool))
            {
                pool.Add(value, parentValue);
            }
        }

        public object GetTargetForAnnotation(AnnotationKey key, object value)
        {
            if (key == null || value == null) return null;

            //try to get the pool for the given key
            AnnotationPool pool;
            if (this._keyPoolMap.TryGetValue(key, out pool))
            {
                //get the graph node for the key
                //NOTE: this *should* exist
                var dependencyNode = this._dependencyGraph.GetNodeForKey(key);

                //try to get pool item for value - if no value was found then target value cannot be found
                var poolItem = pool[value];
                if (poolItem == null) return null;

                if (dependencyNode.Parent == null)
                {
                    //if the node has no parent then the key corresponds to the target - therefore value corresponds to the target value to find
                    return poolItem.Value;
                }
                else
                {
                    //the node has a parent value - if the pool item has a parent value then continue the search with the parent key/value
                    var parentValue = poolItem.ParentValue;
                    return parentValue == null ? null : this.GetTargetForAnnotation(parentValue.Key, parentValue.Value);
                }
            }
            else
            {
                //pool does not exist so target cannot be found
                return null;
            }
        }

        public object GetAnnotationForTarget(AnnotationKey annotationKey, object rootValue)
        {
            if (annotationKey == null || rootValue == null) return null;

            //try to get a path from the root to the target annotation key
            var path = this.GetPathFromRoot(annotationKey);

            if (path == null) return null;
            Debug.Assert(path.Count > 0);

            //path was found so attempt to traverse it - at each stage we need to get the pool item in the child pool that has a parent matching the current 
            //value. If at any stage the child item cannot be found then the target annotation is missing or orphaned.
            var currentKeyNode = path.First;
            var currentKey = currentKeyNode.Value;
            object currentValue = rootValue;
            var currentChildNode = currentKeyNode.Next;

            while (currentChildNode != null)
            {
                AnnotationPool childPool = this._keyPoolMap[currentChildNode.Value];

                //try to get the child item for the current value - if it is not found the target annotation cannot be found
                AnnotationPoolItem childItem = childPool.GetForParent(new AnnotationValue(currentKey, currentValue));
                if (childItem == null) return null;

                currentValue = childItem.Value;
                currentKeyNode = currentChildNode;
                currentChildNode = currentKeyNode.Next;
                currentKey = currentKeyNode.Value;
            }

            //at this point the current key should correspond to the target key
            Debug.Assert(currentKey == annotationKey);
            AnnotationPool targetPool = this._keyPoolMap[currentKey];
            var targetPoolItem = targetPool[currentValue];

            return targetPoolItem == null ? null : targetPoolItem.Value;
        }

        private LinkedList<AnnotationKey> GetPathFromRoot(AnnotationKey target)
        {
            Debug.Assert(target != null);

            //if there is no pool for the target key then there is no possible path
            if (!this._keyPoolMap.ContainsKey(target)) return null;

            return this.GetPathFromRoot(target, new LinkedList<AnnotationKey>());
        }

        private LinkedList<AnnotationKey> GetPathFromRoot(AnnotationKey target, LinkedList<AnnotationKey> currentPath)
        {
            Debug.Assert(target != null);
            Debug.Assert(currentPath != null);

            //add the current target to the list and get the corresponding dependency node
            currentPath.AddFirst(target);
            var node = this._dependencyGraph.GetNodeForKey(target);

            return node.Parent == null
                ? currentPath
                : this.GetPathFromRoot(node.Parent.Value, currentPath);
        }
    }
}
